using System;
using Microsoft.SPOT;
using System.Collections;
using System.Net;
using System.Threading;
using System.Net.Sockets;
using System.IO;
using System.Text;

namespace STOMP
{
    public class STOMP
    {

        private Hashtable handlers = new Hashtable();
        private readonly static char[] INVALID_ADDRESS_CHARS = {' ', '#', '*', ',', '?', '[', ']', '{', '}' };

        /// <summary>
        /// Destination IP of transmitter (if started).
        /// </summary>
        public EndPoint DestinationIP { get; private set; }

        System.Net.Sockets.Socket connection = new System.Net.Sockets.Socket(System.Net.Sockets.AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp);

        public STOMP()
        {
            //check valid net connection
        }

        public delegate void STOMPMessageEventHandler(STOMP sender, string address, string message);
        public delegate void STOMPConnectedEventHandler(STOMP sender);


        private STOMPConnectedEventHandler onConnected;

        public class STOMPReceivedHandler
        {
            private STOMPMessageEventHandler onReceive;
            /// <summary>
            /// Address pattern on the server which can be matched. Currently only wildcard address are supported within incoming packets e.g. '/*'
            /// </summary>
            public string Queue { get; private set; }

            /// <summary>
            /// Creates a handler which fires an event when matching OSC messages are received.
            /// </summary>
            /// <param name="address">Address for messages to match and fire this event e.g. '/guitar/1'</param>
            public STOMPReceivedHandler(string queue, Socket connection)
            {
                if (queue.IndexOfAny(INVALID_ADDRESS_CHARS) != -1)
                    throw new Exception("Only text characters are valid.");
                Queue = queue;

                this.connection = connection;
            }

            private Socket connection;

            public string ID { get; set; }

            public void UnSubscribe()
            {
                string send ="UNSUBSCRIBE" + "\n";
                if (ID != null && ID != "")
                    send += "id:" + ID;
                else
                    send += "destination:" + Queue;
                send += "\n";
                send += 0x0;
                connection.Send(Encoding.UTF8.GetBytes(send));
            }

            /// <summary>
            /// Event fired when a message matching the address is received.
            /// </summary>
            public event STOMPMessageEventHandler MessageReceived;

            internal void Invoke(STOMP sender, string address, string items)
            {
                //invoke this event...
                if (this.onReceive == null)
                {
                    this.onReceive = new STOMPMessageEventHandler(this.Invoke);
                }

                if (Program.CheckAndInvoke(MessageReceived, this.onReceive, sender, address, items))
                {
                    this.MessageReceived(sender, address, items);
                }
            }
        }

        public event STOMPConnectedEventHandler Connected;

        internal void InvokeConnected(STOMP sender)
        {
            //invoke this event...
            if (this.onConnected == null)
            {
                this.onConnected = new STOMPConnectedEventHandler(this.InvokeConnected);
            }

            if (Program.CheckAndInvoke(Connected, this.onConnected, sender))
            {
                this.Connected(sender);
            }
        }

        public string SessionID { get; private set; }

        private void SendConnect(string username, string password)
        {
            //transmit connection message...
            string send =  "CONNECT"+ "\n";
            //send += "accept-version:1.0";
            if (username != null)
            {
                send += "login:" + username;
                send += "passcode:" + password;
            }
            send += "\n";
            send += '\0';
            connection.Send(Encoding.UTF8.GetBytes(send));
        }

        //TODO this might not work for TCP connections!!!
        private void ConnectMe(string server, int port)
        {
            connection.Connect(new IPEndPoint(IPAddress.Parse(server), port));
            Thread rthread = new Thread(new ThreadStart(() =>
            {
                try
                {
                    byte[] buffer = new byte[1024];
                    while (true)
                    {
                        //if (connection.Poll(-1, SelectMode.SelectRead))
                        //{
                            int byteCount = connection.Receive(buffer, 0);
                            //byte[] buffer = new byte[connection.Available];
                            //int bytesRead = connection.Receive(buffer);

                            //do somthing with the packet...
                            if (byteCount > 0)
                                ProcessMessage(buffer);
                        //}
                        //else
                        //{
                         //   Thread.Sleep(10);
                        //}
                    }
                }
                finally
                {
                    throw new Exception("Connection Error");
                }
            }));
            rthread.Start();
        }

        public void Connect(string server, int port, string username, string password)
        {
            ConnectMe(server, port);
            SendConnect(username, password);
        }

        public void Connect(string server, int port)
        {
            ConnectMe(server,port);
            SendConnect(null, null);
        }

        public bool IsConnected {get; private set;}

        private Hashtable GetHeaders(StreamReader reader)
        {
            var temp = new Hashtable();
            string s;
            while ((s = reader.ReadLine()) != "")
            {
                var t = s.Split(':');
                temp.Add((t[0] as string).Trim(), (t[1] as string).Trim());
            }
            return temp;
        }

        private void ProcessMessage(byte[] data)
        {
            //read message from server...
            //if connected message, then connected = true;
            StreamReader reader = new StreamReader(new MemoryStream(data));
            string command = reader.ReadLine().Trim();
            var headers = GetHeaders(reader);
            if (command == "CONNECTED")
            {
                //process connected frame
                IsConnected = true;
                if (headers.Contains("session"))
                    SessionID = headers["session"].ToString();
                InvokeConnected(this);
                return;
            }

            if (command == "MESSAGE")
            {
                //get body...
                string body = reader.ReadToEnd().TrimEnd('\n','\r',(char)0x0);

                //process connected frame
                if (headers.Contains("subscription"))
                {
                    (handlers[headers["subscription"]] as STOMPReceivedHandler).Invoke(this, headers["destination"].ToString(), body);
                    return;
                }
                else
                {
                    foreach (var h in handlers)
                    {
                        if ((h as STOMPReceivedHandler).Queue == headers["destination"].ToString())
                        {
                            (h as STOMPReceivedHandler).Invoke(this, headers["destination"].ToString(), body);
                            return;
                        }
                    }
                }

                return;
            }

            if (command == "ERROR")
            {
                string body = reader.ReadToEnd().TrimEnd('\n','\r',(char)0x0);
                throw new Exception(headers["message"].ToString() + ": " + body);
            }
        }

        public void Disconnect()
        {
            string send = "DISCONNECT" + "\n";
            send += "\n";
            send += 0x0;
            connection.Send(Encoding.UTF8.GetBytes(send));
        }

        private int currentuniqueid = 0;

        public STOMPReceivedHandler Subscribe(string queue)
        {
            if (queue.IndexOfAny(INVALID_ADDRESS_CHARS) != -1)
                throw new Exception("Only text characters are valid.");

            if (!IsConnected)
                throw new Exception("Not connected to STOMP server.");

            var hand = new STOMPReceivedHandler(queue,connection);

            hand.ID = (currentuniqueid++).ToString();

            string send = "SUBSCRIBE" + "\n";
            send += "destination:" + queue;
            send += "id:" + hand.ID; 
            send += "\n";
            send += 0x0;
            connection.Send(Encoding.UTF8.GetBytes(send));

            handlers.Add(hand.ID, hand);
            return hand;
        }

        public void Send(string queue, string message)
        {
            if (queue.IndexOfAny(INVALID_ADDRESS_CHARS) != -1)
                throw new Exception("Only text characters are valid.");

            if (IsConnected)
            {
                string send = "SEND" + "\n";
                send += "destination:" + queue;
                send += "\n";
                send += message;
                send += 0x0;
                connection.Send(Encoding.UTF8.GetBytes(send));
            }
        }
    }
}
